#include "field.hpp"
#include "sourceField.hpp"
#include "fieldFunctions.hpp"
#include "meshFunctions.hpp"
#include "meshTypeFunctions.hpp"
#include "structMesh_testFunctions.hpp"
#include "typeHelpers.hpp"

#include "testHelpers.hpp"
#include <catch2/catch_test_macros.hpp>


using namespace hms;


TEST_CASE( "Creating constant SourceField" ){

	auto test = []( auto&& dummyMesh ){
		using MeshType = remove_qualifiers<decltype(dummyMesh)>;
		auto mesh { uniMeshAs<MeshType>(3, 4, 1, 1, true) };

		SECTION( "Uniform" ){
			using SField = SourceField<Uniform, MeshType>;
			ArrayXXs values (2, 1), valuesCopy;
			values << 0.1, 0.3;
			valuesCopy = values;

			auto testValues = [&]( const SField& field ){
				REQUIRE( field.updateMethod() == UpdateMethod::none );
				for ( int i{0}; i < mesh.nDomainCells(); ++i )
					REQUIRE( (field.domainValues().col(i) == values).all() );
			};

			SECTION( "Default initialisation" ){
				SField uf;
				uf.mesh(mesh);
				uf.values( std::move(valuesCopy) );
				testValues(uf);
			}
			SECTION( "Full initialisation" ){
				SField uf {
					mesh, std::move(valuesCopy)
				};
				testValues(uf);
			}
		}
		SECTION( "Non-Uniform" ){
			using SField = SourceField<NonUniform, MeshType>;
			ArrayXXs values ( 2, nFieldCols(mesh) ), valuesCopy;
			values.setRandom();
			setCorners(values, mesh, 
				FieldBase<SourceField<NonUniform, MeshType>>::cornerValue
			);
			valuesCopy = values;

			auto testValues = [&]( const SField& field ){
				REQUIRE( field.updateMethod() == UpdateMethod::none );
				REQUIRE( (field.values() == values).all() );
			};

			SECTION( "Default initialisation" ){
				SField nuf;
				nuf.mesh(mesh);
				nuf.values( std::move(valuesCopy) );
				testValues(nuf);
			}
			SECTION( "Full initialisation" ){
				SField nuf {
					mesh, std::move(valuesCopy)
				};
				testValues(nuf);
			}
		}
	};
	TEST_ALL_MESHTYPES(test)
}


TEST_CASE( "Creating SourceField with time series" ){

	auto test = []( auto&& dummyMesh ){
		using MeshType = remove_qualifiers<decltype(dummyMesh)>;
		auto mesh { uniMeshAs<MeshType>(3, 4, 1, 1, true) };

		Time time {10};
		time.maxTimeStep(10);
		time.writeTimeInterval(10);

		SECTION( "Uniform" ){
			using SField = SourceField<Uniform, MeshType>;
			Array2s values0 { 0.1, 0.3 }, values1 { 0.3, 0.1 };
			TimeSeries timeSeries {
				{0, values0}, {2, values1}
			};

			auto testValues = []( SField& field, ArrayXs values ){
				REQUIRE( field.updateMethod() == UpdateMethod::timeSeries );
				field.updateValues();
				for ( int i{0}; i < field.domainValues().cols(); ++i )
					REQUIRE( field.domainValues().col(i).isApprox(values) );
			};
			SECTION("No interpolation"){
				SECTION( "Default initialisation" ){
					SField uf;
					uf.mesh(mesh);
					uf.timeSeries( std::move(timeSeries) );
					uf.time( time );
					REQUIRE( !uf.useInterpolation() );
					testValues(uf, values0);
					time.advanceBy(1);
					testValues(uf, values0);
					time.advanceBy(1);
					testValues(uf, values1);
				}
				SECTION( "Full initialisation" ){
					SField uf {
						mesh, std::move(timeSeries), time
					};
					REQUIRE( !uf.useInterpolation() );
					testValues(uf, values0);
					time.advanceBy(1);
					testValues(uf, values0);
					time.advanceBy(1);
					testValues(uf, values1);
				}
			}
			SECTION("With interpolation"){
				SECTION( "Default initialisation" ){
					SField uf;
					uf.mesh(mesh);
					uf.timeSeries( std::move(timeSeries) );
					uf.time( time );
					uf.useInterpolation(true);
					REQUIRE( uf.useInterpolation() );
					testValues(uf, values0);
					time.advanceBy(1);
					testValues(uf, Array2s{0.2, 0.2});
					time.advanceBy(1);
					testValues(uf, values1);
				}
				SECTION( "Full initialisation" ){
					SField uf {
						mesh, std::move(timeSeries), time, true
					};
					REQUIRE( uf.useInterpolation() );
					testValues(uf, values0);
					time.advanceBy(1);
					testValues(uf, Array2s{0.2, 0.2});
					time.advanceBy(1);
					testValues(uf, values1);
				}
			}
		}
		SECTION( "Non-Uniform" ){
			using SField = SourceField<NonUniform, MeshType>;
			ArrayXXs values0 ( 2, mesh.nDomainCells() ), values1, values2;
			values1.resizeLike(values0);
			values2.resizeLike(values0);

			values0 <<
				 0,  2,  4,  6,  8, 10, 12, 14, 16, 18, 20, 22,
				22, 20, 18, 16, 14, 12, 10,  8,  6,  4,  2,  0;
			values1 = values0 + 1;
			values2 = values0 + 2;

			ArrayXXs timeValues0 (2, nFieldCols(mesh) ), timeValues2;
			timeValues2.resizeLike(timeValues0);

			domainValues( timeValues0, mesh ) = values0;
			domainValues( timeValues2, mesh ) = values2;

			TimeSeries timeSeries {
				{0, std::move(timeValues0)},
				{2, std::move(timeValues2)}
			};

			auto testValues = [&]( SField& field, const ArrayXXs& values ){
				REQUIRE( field.updateMethod() == UpdateMethod::timeSeries );
				field.updateValues();
				REQUIRE( field.domainValues().isApprox(values) );
			};

			SECTION("No interpolation"){
				SECTION( "Default initialisation" ){
					SField nuf;
					nuf.mesh(mesh);
					nuf.timeSeries( std::move(timeSeries) );
					nuf.time(time);
					REQUIRE( !nuf.useInterpolation() );
					testValues(nuf, values0);
					time.advanceBy(1);
					testValues(nuf, values0);
					time.advanceBy(1);
					testValues(nuf, values2);
				}
				SECTION( "Full initialisation" ){
					SField nuf {
						mesh, std::move(timeSeries), time
					};
					REQUIRE( !nuf.useInterpolation() );
					testValues(nuf, values0);
					time.advanceBy(1);
					testValues(nuf, values0);
					time.advanceBy(1);
					testValues(nuf, values2);
				}
			}
			SECTION("With interpolation"){
				SECTION( "Default initialisation" ){
					SField nuf;
					nuf.mesh(mesh);
					nuf.timeSeries( std::move(timeSeries) );
					nuf.time(time);
					nuf.useInterpolation(true);
					REQUIRE( nuf.useInterpolation() );
					testValues(nuf, values0);
					time.advanceBy(1);
					testValues(nuf, values1);
					time.advanceBy(1);
					testValues(nuf, values2);
				}
				SECTION( "Full initialisation" ){
					SField nuf {
						mesh, std::move(timeSeries), time, true
					};
					REQUIRE( nuf.useInterpolation() );
					testValues(nuf, values0);
					time.advanceBy(1);
					testValues(nuf, values1);
					time.advanceBy(1);
					testValues(nuf, values2);
				}
			}
		}
	};
	TEST_ALL_MESHTYPES(test)
}

TEST_CASE( "Creating SourceField with function parsers" ){

	auto test = []( auto&& dummyMesh ){
		using MeshType = remove_qualifiers<decltype(dummyMesh)>;
		auto mesh { uniMeshAs<MeshType>(3, 4, 1, 1) };

		Time time {10};
		time.maxTimeStep(10);
		time.writeTimeInterval(10);

		SECTION( "Uniform" ){
			using SField = SourceField<Uniform, MeshType>;

			std::vector<FunctionParserWrapper> func {
				{"g*t"},
				{"sin(t)"}
			};

			Array2s values;

			auto testValues = [&]( SField& field ){
				REQUIRE( field.updateMethod() == UpdateMethod::function );
				field.updateValues();
				for ( Index i{0}; i < field.values().cols(); ++i )
					REQUIRE( (field.values().col(i) == values).all() );
			};

			SECTION( "Default initialisation" ){
				SField uf;
				uf.mesh(mesh);
				uf.functions( std::move(func) );
				uf.time(time);
				values = 0;
				testValues(uf);
				time.advanceBy(1);
				values << phys::g, std::sin(1);
				testValues(uf);
			}
			SECTION( "Full initialisation" ){
				SField uf {
					mesh, std::move(func), time
				};
				values = 0;
				testValues(uf);
				time.advanceBy(2);
				values << 2*phys::g, std::sin(2);
				testValues(uf);
			}
		}
		SECTION( "Non-Uniform" ){
			using SField = SourceField<NonUniform, MeshType>;

			std::vector<FunctionParserWrapper> func {
				{"x*g*t"},
				{"y*sin(t)"}
			};

			ArrayXXs values0, values1, values2;

			values0.resize(2, mesh.nDomainCells() );
			values1.resizeLike(values0);

			values1.row(0).reshaped(3,4).colwise() = Array3s{0.5,1.5,2.5};
			values1.row(1).reshaped(3,4).rowwise() = RowArray4s{0.5,1.5,2.5,3.5};
			values2 = values1;

			values0 = 0;
			values1.colwise() *= Array2s{ phys::g, std::sin(1) };
			values2.colwise() *= Array2s{ phys::g*2, std::sin(2) };


			auto testValues = [&]( SField& field, const ArrayXXs values ){
				REQUIRE( field.updateMethod() == UpdateMethod::function );
				field.updateValues();
				REQUIRE( field.domainValues().isApprox(values) );
			};

			Field<MeshType> zField {mesh, 1};
			zField.values() = 0;

			SECTION( "Default initialisation" ){
				SField sf;
				sf.mesh(mesh);
				sf.functions( std::move(func) );
				sf.time(time);
				sf.zField(zField);
				testValues(sf, values0);
				time.advanceBy(1);
				testValues(sf, values1);
				time.advanceBy(1);
				testValues(sf, values2);
			}
			SECTION( "Full initialisation" ){
				SField sf {
					mesh, std::move(func), time, zField
				};
				testValues(sf, values0);
				time.advanceBy(1);
				testValues(sf, values1);
				time.advanceBy(1);
				testValues(sf, values2);
			}
		}
	};
	TEST_ALL_MESHTYPES(test)
}

TEST_CASE("Creating source field with callable"){

	auto test = []( auto&& dummyMesh ){
		using MeshType = remove_qualifiers<decltype(dummyMesh)>;
		auto mesh { uniMeshAs<MeshType>(3, 4, 1, 1) };
		Time time {10};
		time.maxTimeStep(10);
		time.writeTimeInterval(10);

		bool isUniform {true};
		ArrayXXs values;

		auto updater = [&](ArrayXXs& vals) -> void {
			vals = values;
		};

		auto testValues = [&]( auto& field ){
			if (isUniform)
				values.resize(2,1);
			else
				values.resize( 2, nFieldCols(mesh) );
			values.setRandom();
			field.updateValues();
			if (isUniform){
				for ( int i{0}; i < mesh.nDomainCells(); ++i )
					REQUIRE( field.domainValues().col(i).isApprox(values) );
			} else {
				REQUIRE( field.domainValues().isApprox(
					domainValues(values, mesh) )
				);
			}
		};

		SECTION("Uniform"){
			using SField = SourceField<Uniform, MeshType>;
			SECTION( "Default initialisation" ){
				SField sf;
				sf.mesh(mesh);
				sf.callable( std::move(updater), 2 );
				testValues(sf);
			}
			SECTION( "Full initialisation" ){
				SField sf {mesh, std::move(updater), 2 };
				testValues(sf);
			}
		}
		SECTION("Non-uniform"){
			using SField = SourceField<NonUniform, MeshType>;
			isUniform = false;
			SECTION( "Default initialisation" ){
				SField sf;
				sf.mesh(mesh);
				sf.callable( std::move(updater), 2 );
				testValues(sf);
			}
			SECTION( "Full initialisation" ){
				SField sf {mesh, std::move(updater), 2};
				testValues(sf);
			}
		}
	};
	TEST_ALL_MESHTYPES(test)
}